#!/usr/bin/env python3

# -*- coding: utf-8 -*-

# This tool generate an apt local repository.

# This tool parses the seed deb files in the folder {target_deb_dir_path}.
# It extract the depends info (include 'depends', 'pre-depends', but not
# 'recommends', 'suggests'), and find the depend deb package in the folder
# {mirror_path}. (You should set the apt mirro config in /etc/apt/mirror.list,
# and get the apt repository mirror with command `sudo apt-mirror` first.) Then
# it copy the depend deb package files into the folder {target_deb_dir_path}.
# Then it generate local apt repository config with command `dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz`.
# After that, it create local apt repository source file "/etc/apt/sources.list.d/audiolocalrepo.list" in
# the folder '/etc/apt/sources.list.d/'. Then run the command `sudo apt update`,
# the debs in the {target_deb_dir_path} should exist in the deb list in synaptic.

# The python package 'debian' comes from 'python-debian', you can install it with
# command 'sudo pip install python-debian'.

import argparse
import logging
import os
import re
import shutil
import subprocess
import sys

from debian import debfile
from datetime import datetime


audiolocalrepo_list_path = "/etc/apt/sources.list.d/audiolocalrepo.list"


def log(message):
    logging.info(message)
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    print(f"[{timestamp}] {message}")


def parse_pkg_list(pkg_list):
    if not pkg_list:
        return []

    pattern = re.compile('(.+)\s(\(.+\))$')
    packages = {}
    pkgs = pkg_list.split(',')

    package_list = []
    package_list.extend(pkgs)
    index = 0
    # log(f"pkgs %s." % str(pkgs))

    while index < len(package_list):
        pkg = package_list[index]
        pkg = pkg.strip()
        matcher = pattern.match(pkg)

        if matcher:
            pkgname = matcher.group(1)
            version = matcher.group(2)
            #  log(f"match pkg ({pkgname} <--> {version}).")
            packages[pkgname] = version
        else: ## Maybe it is multiple packages.
            if "|" in pkg:
                # log(f"Multi package: {pkg}.")
                pass
            packages[pkg] = ""
            # log(f"No version package: {pkg}.")

        index += 1

    return packages


def parse_deb_file(deb_file_path):
    ignored_items = set(['description', 'architecture', 'maintainer', 'installed-size', 'source',
                      'section', 'priority', 'multi-arch', 'homepage', 'original-maintainer',
                      'oe', 'packagearch', 'breaks', 'replaces', 'conflicts', 'built-using',
                      'original-vcs-browser', 'original-vcs-git', 'enhances', 'gstreamer-decoders',
                      'gstreamer-elements', 'gstreamer-uri-sinks', 'gstreamer-uri-sources',
                      'gstreamer-version', 'gstreamer-encoders', 'ruby-versions', 'essential',
                      'cnf-visible-pkgname', 'protected', 'important'
    ])

    # log(f"deb_file_path \"{deb_file_path}\".")
    deb = debfile.DebFile(deb_file_path)

    package = None
    version = None
    pre_depends = None
    depends = None
    recommends = None
    suggests = None
    provides = None

    pkg_info = {}

    for n, v in deb.debcontrol().items():
        item = n.lower()
        if item in ignored_items:
            continue

        if item == 'package':
            package = v
        elif item == 'version':
            version = v
        elif item == 'depends':
            depends = v
        elif item == 'pre-depends':
            pre_depends = v
        elif item == 'recommends':
            recommends = v
        elif item == 'suggests':
            suggests = v
        elif item == 'provides':
            provides = v
        else:
            print(' Other %s: %s' % (n, v))

    if not package:
        return (None, None)

    pkg_info['filepath'] = deb_file_path

    if version:
        pkg_info['version'] = version
    if depends:
        pkg_info['depends'] = parse_pkg_list(depends)
    if pre_depends:
        pkg_info['pre_depends'] = parse_pkg_list(pre_depends)
    if recommends:
        pkg_info['recommends'] = parse_pkg_list(recommends)
    if suggests:
        pkg_info['suggests'] = parse_pkg_list(suggests)
    if provides:
        pkg_info['provides'] = parse_pkg_list(provides)

    return (package, pkg_info)


def find_depend(mirror_path, package):
    deb_dir_list = []
    deb_dir_list.append(mirror_path)
    while len(deb_dir_list) > 0:
        dir_path = deb_dir_list.pop()
        dir_files = os.listdir(dir_path)
        for deb_file in dir_files:
            deb_file_path = os.path.join(dir_path, deb_file)
            if os.path.isfile(deb_file_path):
                if deb_file.startswith(package+"_") and deb_file.endswith(".deb"):
                    return deb_file, deb_file_path
            elif os.path.isdir(deb_file_path):
                deb_dir_list.append(deb_file_path)

    return None, None


def copy_deb_file(depend_deb_file_path, target_deb_dir_path, file_name):
    # Copy deb file
    target_deb_file_path = os.path.join(target_deb_dir_path, file_name)
    if os.path.exists(target_deb_file_path):
        log(f"Depend deb file \"{target_deb_file_path}\" exists.")
        return None
    shutil.copy2(depend_deb_file_path, target_deb_file_path)
    log(f"Find depend deb file \"{file_name}\", \"{depend_deb_file_path}\" => \"{target_deb_file_path}\".")
    if not os.path.exists(target_deb_file_path):
        log(f"Copy depend deb file \"{target_deb_file_path}\" failed.")
        return None

    return target_deb_file_path


def resolve_depend_list(deb_packages, unsatisfied_pkgs, mirror_path, target_deb_dir_path, depend_list):
    result = True
    if not depend_list:
        return result, None, unsatisfied_pkgs
    
    depend_file_list = []
    for depend_pkg, dpend_ver in depend_list.items():
        if depend_pkg in deb_packages:
            # log(f"Package \"{depend_pkg}\" exist.")
            continue

        # For example "perl:any", python3:any
        if ":" in depend_pkg:
            depend_pkg = depend_pkg.split(":")[0]

        if depend_pkg in deb_packages:
            # log(f"Package \"{depend_pkg}\" exist.")
            continue

        if depend_pkg in unsatisfied_pkgs:
            # log(f"Package \"{depend_pkg}\" cannot found.")
            continue

        if "|" in depend_pkg:
            found = False
            tmp_pkg_list = parse_pkg_list(','.join(depend_pkg.split('|')))
            for pkg_name, pkg_ver in tmp_pkg_list.items():
                if pkg_name in deb_packages:
                    found = True
                    continue

                if pkg_name in unsatisfied_pkgs:
                    continue

                file_name, depend_deb_file_path = find_depend(mirror_path, pkg_name)
                if file_name:
                    found = True
                    target_deb_file_path = copy_deb_file(depend_deb_file_path, target_deb_dir_path, file_name)
                    if target_deb_file_path:
                        depend_file_list.append(target_deb_file_path)
            if not found:
                result = False
                if depend_pkg not in unsatisfied_pkgs:
                    log(f"Cannot find depend \"{depend_pkg}\".")
                    unsatisfied_pkgs.add(depend_pkg)
        else:
            file_name, depend_deb_file_path = find_depend(mirror_path, depend_pkg)
            if not file_name:
                result = False
                if depend_pkg not in unsatisfied_pkgs:
                    log(f"Cannot find depend \"{depend_pkg}\".")
                    unsatisfied_pkgs.add(depend_pkg)
                continue

            target_deb_file_path = copy_deb_file(depend_deb_file_path, target_deb_dir_path, file_name)
            if target_deb_file_path:
                depend_file_list.append(target_deb_file_path)

    return result, depend_file_list, unsatisfied_pkgs


def resolve_package_depends(deb_packages, unsatisfied_pkgs, mirror_path, target_deb_dir_path, pkg_info):
    result = True
    if not pkg_info:
        return result, None, unsatisfied_pkgs

    # ['depends', 'pre_depends', 'recommends', 'suggests']
    depends_type_list = ['depends', 'pre_depends']
    depends_file_list = []

    for depends_type in depends_type_list:
        if depends_type in pkg_info:
            tmpresult, depend_file_list, unsatisfied_pkgs = resolve_depend_list(deb_packages, unsatisfied_pkgs, mirror_path,
                                                                                target_deb_dir_path, pkg_info[depends_type])
            if depend_file_list:
                depends_file_list.extend(depend_file_list)
            if not tmpresult:
                result = tmpresult

    return result, depends_file_list, unsatisfied_pkgs


def parse_and_add_deb_package_if_needed(deb_packages, deb_file_path):
    # log(f"deb_file_path \"{deb_file_path}\".")
    package, pkg_info = parse_deb_file(deb_file_path)
    if not package:
        return package, pkg_info, deb_packages

    if package not in deb_packages:
        deb_packages[package] = pkg_info
        # log("Add package: \"%s\"." % str(deb_packages[package]))

        if 'provides' in pkg_info:
            provide_list = pkg_info['provides']
            for provide_pkg, provide_ver in provide_list.items():
                if provide_pkg in deb_packages:
                    # log(f"Package \"{provide_pkg}\" exist.")
                    continue
                deb_packages[provide_pkg] = pkg_info
    else:
        # log("Package: \"%s\"(\"%s\"): \"%s\"." %
        #         (package, deb_file_path, str(deb_packages[package])))
        pass

    return package, pkg_info, deb_packages


def run_command(command, success_msg, failure_msg):
    result = subprocess.run(command, shell=True)
    if result.returncode == 0:
        log(success_msg)
        return True
    else:
        log(failure_msg)
        return False


def check_app_installed(package_name):
    check_app = subprocess.run(f"dpkg -s {package_name}", shell=True, capture_output=True, text=True)
    if check_app.returncode != 0:
        log(f"{package_name} is not installed...")
        return False

    return True


def update_package_list():
    log("Start to update apt deb list...")
    run_command(f"sudo apt update", "Update successfully", "Update failed")


def generate_localrepo(target_deb_dir_path, gen_audio_local_repo):
    if not check_app_installed("dpkg-dev"):
        log("dpkg-dev is not installed, cannot generate Packages.gz.")
        sys.exit(1)

    log("Start to genrate Packages.gz ...")
    run_command(f"cd {target_deb_dir_path} && dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz",
                "Generate Packages.gz successfully",
                "Generate Packages.gz failed")

    packages_gz_path = os.path.join(target_deb_dir_path, "Packages.gz")
    if not os.path.exists(packages_gz_path):
        log(f"Directory \'{target_deb_dir_path}\' has no Packages.gz, cannot generate audiolocalrepo.list.")
        sys.exit(1)

    run_command(f"echo 'deb [trusted=yes] file:{target_deb_dir_path}/ ./' | sudo tee {audiolocalrepo_list_path}",
                "Generate audiolocalrepo.list successfully",
                "Generate audiolocalrepo.list failed")

    # update_package_list()


def main(argsparser):
    args = parser.parse_args()

    mirror_path = args.mirror_path
    target_deb_dir_path = args.target_deb_dir_path
    gen_audio_local_repo = args.gen_audio_local_repo
    
    deb_packages = {}
    deb_files_list = []

    deb_dir_list = []

    # print(f"mirror_path : {mirror_path}, target_deb_dir_path : {target_deb_dir_path}, \
    #         gen_audio_local_repo : {gen_audio_local_repo}")
    if not mirror_path or not os.path.exists(mirror_path):
        log(f"mirror_path \"{mirror_path}\" does not exist.")
        argsparser.print_help()
        sys.exit(1)

    if not target_deb_dir_path or not os.path.exists(target_deb_dir_path):
        log(f"target_deb_path \"{target_deb_dir_path}\" does not exist.")
        argsparser.print_help()
        sys.exit(1)

    if not gen_audio_local_repo or not os.path.exists(gen_audio_local_repo):
        log(f"Audio local repo \"{gen_audio_local_repo}\" does not exist.")
        argsparser.print_help()
        sys.exit(1)
    
    deb_dir_list.append(target_deb_dir_path)
    while len(deb_dir_list) > 0:
        dir_path = deb_dir_list.pop()
        dir_files = os.listdir(dir_path)
        for deb_file in dir_files:
            deb_file_path = os.path.join(dir_path, deb_file)
            if os.path.isfile(deb_file_path) and deb_file.endswith(".deb"):
                deb_files_list.append(deb_file_path)
            elif os.path.isdir(deb_file_path):
                deb_dir_list.append(deb_file_path)

    index = 0
    while index < len(deb_files_list):
        deb_file_path = deb_files_list[index]

        package, pkg_info, deb_packages = parse_and_add_deb_package_if_needed(deb_packages, deb_file_path)
        if not package:
            index += 1
            continue

        index += 1

    unsatisfied_pkgs = set()
    index = 0
    while index < len(deb_files_list):
        deb_file_path = deb_files_list[index]
        log(f"Parse {index + 1}/{len(deb_files_list)} deb file {deb_file_path}.")

        package, pkg_info, deb_packages = parse_and_add_deb_package_if_needed(deb_packages, deb_file_path)
        if not package:
            # log(f"{deb_file_path} lack of package name.")
            index += 1
            continue

        result, depend_file_list, unsatisfied_pkgs = resolve_package_depends(
            deb_packages, unsatisfied_pkgs, mirror_path, target_deb_dir_path, pkg_info)

        if depend_file_list:
            deb_files_list.extend(depend_file_list)
        if not result:
            log("Resolve package depends failed: \"%s\"." % package)

        print("")
        index += 1

    log(f"Unsatisfied depends: \"{unsatisfied_pkgs}\".")

    generate_localrepo(target_deb_dir_path, gen_audio_local_repo)


if __name__ == "__main__":
    class CustomHelpFormatter(argparse.RawTextHelpFormatter):
        def format_help(self):
            help_text = super().format_help()
            return help_text

    parser = argparse.ArgumentParser(
        description='Manage audio local repository packages.',
        formatter_class=CustomHelpFormatter
    )

    parser.add_argument('--mirror_path',          type=str, required=False,   help='Set the mirror debs repository path.')
    parser.add_argument('--target_deb_dir_path',  type=str, required=False,   help='Set the target debs repository path.')
    parser.add_argument('--view_deb_info',        type=str, required=False,   help='Check the debs info in the specific directory.')
    parser.add_argument('--gen_audio_local_repo', type=str, required=False,   help='Set the debs directory, and generate local repository from target_deb_path.')
    parser.add_argument('--rm_audio_local_repo',  action='store_true',        help='Remove the generated local debs repository.')

    main(parser)
